package cn.lg.soar.system.biz.modules.general.service;

import cn.lg.soar.common.model.PageQuery;
import cn.lg.soar.common.model.PageResult;
import cn.lg.soar.common.util.AssertUtil;
import cn.lg.soar.common.util.DatetimeUtil;
import cn.lg.soar.common.util.FileUtil;
import cn.lg.soar.common.util.PojoUtil;
import cn.lg.soar.common.util.data.DataUtil;
import cn.lg.soar.database.query.QueryParams;
import cn.lg.soar.database.query.SqueryWapper;
import cn.lg.soar.system.api.enums.AccessModeEnum;
import cn.lg.soar.system.api.model.FastUploadVO;
import cn.lg.soar.system.api.model.FileVO;
import cn.lg.soar.system.biz.config.SystemProps;
import cn.lg.soar.system.biz.filetemplate.FileTemplate;
import cn.lg.soar.system.biz.filetemplate.FileTemplateManager;
import cn.lg.soar.system.biz.filetemplate.model.UploadCompleteVO;
import cn.lg.soar.system.biz.filetemplate.model.UploadInfoVO;
import cn.lg.soar.system.biz.filetemplate.model.UploadInitVO;
import cn.lg.soar.system.biz.modules.general.entity.FileMetadata;
import cn.lg.soar.system.biz.modules.general.entity.FileStorage;
import cn.lg.soar.system.biz.modules.general.entity.FileUploadInfo;
import cn.lg.soar.system.biz.modules.general.model.FileMetadataVO;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.MessageFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.UUID;

/**
 * @author luguoxiang 469599794@qq.com
 * @Date: 2024/6/1 13:17
 * 开源项目：https://gitee.com/lgx1992/lg-soar 求star！请给我star！请帮我点个star！
 */
public class FileService {

    private static final Logger LOGGER = LoggerFactory.getLogger(FileService.class);

    @Autowired
    private IFileStorageService fileStorageService;
    @Autowired
    private IFileMetadataService fileMetadataService;
    @Autowired
    private FileTemplateManager fileTemplateManager;
    @Autowired
    private SystemProps props;
    @Autowired
    private IFileUploadInfoService uploadInfoService;

    /**
     * 文件上传
     * @param file
     * @param directory
     * @desc
     * 开始上传
     * │
     * ├─▶ 获取文件名、后缀、大小
     * │
     * ├─▶ 校验文件类型（黑名单/白名单）
     * │
     * ├─▶ 写入临时文件
     * │
     * ├─▶ 计算文件 MD5
     * │
     * ├─▶ 构造 code = md5 + size
     * │
     * ├─▶ 查询 FileMetadata 是否存在？
     * │       └── 存在：
     * │               └─▶ 检查目标仓库是否存在该文件？
     * │                       └─▶ 存在：直接返回已有 metadata
     * │                       └─▶ 不存在：准备重新上传
     * │       └── 不存在：
     * │               └─▶ 准备上传新文件
     * │
     * ├─▶ 上传文件到指定仓库（uploadTemplate.upload）
     * │
     * ├─▶ 保存或更新 metadata 到数据库
     * │
     * └─▶ 返回 metadata（包含文件信息）
     */
    @Transactional(rollbackFor = Exception.class)
    public FileMetadata upload(MultipartFile file, String directory) {
        // 文件校验
        String originalFilename = file.getOriginalFilename();
        String suffix = FileUtil.getSuffix(originalFilename);
        AssertUtil.isFalse(props.isUploadBlacklist(suffix), "文件【{0}】类型不允许上传，在黑名单内", originalFilename);
        AssertUtil.isTrue(props.isUploadWhitelist(suffix), "文件【{0}】类型不允许上传，不在白名单内", originalFilename);
        long size = file.getSize();
        File tempFile = null;
        InputStream inputStream = null;
        try {
            // 保存临时文件
            tempFile = File.createTempFile("lg-soar-file", UUID.randomUUID().toString());
            file.transferTo(tempFile);
            // 生成md5
            inputStream = new FileInputStream(tempFile);
            MessageDigest digest = MessageDigest.getInstance("MD5");
            byte[] buffer = new byte[4096];
            int bytesRead;
            while ((bytesRead = inputStream.read(buffer)) != -1) {
                digest.update(buffer, 0, bytesRead);
            }
            String md5 = DataUtil.byteToHex(digest.digest());
            // 检查是否重复
            String code = FileUtil.toCode(md5, size);
            FileTemplate uploadTemplate;
            FileMetadata metadata = fileMetadataService.getByCode(code);
            if (metadata == null) {
                // 元数据不存在，上传到默认仓库
                FileStorage defaulted = fileStorageService.getDefaulted();
                AssertUtil.notNull(defaulted, "未找到默认文件仓库");
                uploadTemplate = fileTemplateManager.getTemplate(defaulted);
                Integer storageId = defaulted.getId();
                // 生成元数据
                metadata = new FileMetadata();
                metadata.setId(IdWorker.getId());
                metadata.setStorageId(storageId);
                metadata.setCode(code);
                metadata.setSuffix(suffix);
                metadata.setSize(size);
            } else {
                // 元数据已存在，检查文件是否存在
                uploadTemplate = fileTemplateManager.getTemplate(metadata.getStorageId());
                if (uploadTemplate.isExist(metadata.filename())) {
                    metadata.setOriginalName(originalFilename);
                    return metadata;
                }
                // 文件不存在，上传到默认仓库
                FileStorage defaulted = fileStorageService.getDefaulted();
                AssertUtil.notNull(defaulted, "未找到默认文件仓库");
                uploadTemplate = fileTemplateManager.getTemplate(defaulted);
                Integer storageId = defaulted.getId();
                metadata.setStorageId(storageId);
            }
            // 非重复文件或文件不存在，正常上传，先入库再上传，上传失败则回滚，保证一致性
            metadata.setOriginalName(originalFilename);
            metadata.setDirectory(directory);
            AssertUtil.isTrue(fileMetadataService.saveOrUpdate(metadata), "文件上传失败");
            // 上传文件
            inputStream.close();
            inputStream = new FileInputStream(tempFile);
            uploadTemplate.upload(metadata.filename(), inputStream);
            return metadata;
        } catch (IOException | NoSuchAlgorithmException e) {
            throw new RuntimeException(e);
        } finally {
                                          if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    LOGGER.error("关闭流失败", e);
                }
            }
            if (tempFile != null) {
                try {
                    tempFile.delete();
                } catch (Exception e) {
                    LOGGER.error("关闭临时文件", e);
                }
            }
        }
    }

    @Transactional(rollbackFor = Exception.class)
    public FileMetadata upload(MultipartFile file) {
        return upload(file, getOrCreateDirectory());
    }

    /**
     * 文件秒传
     * @param vo
     * @return
     */
    public FileMetadata fastUpload(FastUploadVO vo) {
        String code = FileUtil.toCode(vo.getDigest(), vo.getFileSize());
        FileMetadata metadata = fileMetadataService.getByCode(code);
        if (metadata != null) {
            // 判断文件是否存在
            try {
                FileTemplate uploadTemplate = fileTemplateManager.getTemplate(metadata.getStorageId());
                if (uploadTemplate.isExist(metadata.filename())) {
                    return metadata;
                }
            } catch (RuntimeException e) {
                LOGGER.error("检查文件是否存在异常", e);
            }
        }
        return null;
    }

    /**
     * 初始化分片上传
     * @param vo
     * @return
     */
    public UploadInfoVO uploadInit(UploadInitVO vo) {
        String code = FileUtil.toCode(vo.getDigest(), vo.getFileSize());
        UploadInfoVO uploadInfoVO = new UploadInfoVO();
        // 查文件（秒传）
        FileMetadata metadata = fastUpload(vo);
        if (metadata != null) {
            String url = getAccessUrl(metadata.getStorageId(), metadata.filename(), vo.getAccessMode());
            uploadInfoVO.setUrl(url);
            uploadInfoVO.setFileId(metadata.getId());
            return uploadInfoVO;
        }
        // 文件是否正在上传
        FileUploadInfo uploadInfo = uploadInfoService.getByCode(code);
        if (uploadInfo == null) {
            // 文件第一次上传
            String originalFilename = vo.getName();
            String directory = getOrCreateDirectory();
            Long id = IdWorker.getId();
            String suffix = FileUtil.getSuffix(originalFilename);
            AssertUtil.isFalse(props.isUploadBlacklist(suffix), "文件【{0}】类型不允许上传，在黑名单内", originalFilename);
            AssertUtil.isTrue(props.isUploadWhitelist(suffix), "文件【{0}】类型不允许上传，不在白名单内", originalFilename);
            String filename = FileUtil.toFilename(directory, id, suffix);
            FileTemplate template = fileTemplateManager.getTemplate(fileStorageService.getDefaulted());
            uploadInfo = new FileUploadInfo();
            uploadInfo.setCode(code);
            uploadInfo.setOriginalName(originalFilename);
            uploadInfo.setSize(vo.getFileSize());
            uploadInfo.setPartSize(vo.getPartSize());
            uploadInfo.setStorageId(fileStorageService.getDefaulted().getId());
            uploadInfo.setFilename(filename);
            uploadInfo.setUploadId(template.uploadInit(filename));
            AssertUtil.isTrue(uploadInfoService.save(uploadInfo), "操作失败");
        }
        // 返回上传信息
        uploadInfoVO.setUploadId(uploadInfo.getUploadId());
        uploadInfoVO.setFilename(uploadInfo.getFilename());
        uploadInfoVO.setPartSize(uploadInfo.getPartSize());
        return uploadInfoVO;
    }

    /**
     * 获取上传路径
     * @param uploadId
     * @param partNumber
     * @return
     */
    public String getUploadPartUrl(String uploadId, Integer partNumber) {
        FileUploadInfo uploadInfo = uploadInfoService.getById(uploadId);
        AssertUtil.notNull(uploadInfo, "请先初始化");
        FileTemplate template = fileTemplateManager.getTemplate(uploadInfo.getStorageId());
        return template.getUploadUrl(uploadInfo.getFilename(), uploadId, partNumber);
    }

    /**
     * 完成分片上传
     * @param vo
     * @return
     */
    @Transactional(rollbackFor = Exception.class)
    public FileMetadata uploadComplete(UploadCompleteVO vo) {
        String uploadId = vo.getUploadId();
        // 判断分片上传情况
        FileUploadInfo uploadInfo = uploadInfoService.getById(uploadId);
        AssertUtil.notNull(uploadInfo, "请先初始化");
        // 上传模板
        Integer storageId = uploadInfo.getStorageId();
        FileTemplate template = fileTemplateManager.getTemplate(storageId);
        AssertUtil.notNull(template, "<UNK>");
        // 解析后缀
        String filename = uploadInfo.getFilename();
        int suffixIndex = filename.lastIndexOf(".");
        String suffix = filename.substring(suffixIndex + 1);
        filename = filename.substring(0, suffixIndex);
        // 解析目录和id
        int i = filename.lastIndexOf("/");
        String directory = filename.substring(0, i);
        // 文件元数据
        String code = uploadInfo.getCode();
        FileMetadata fileMetadata = fileMetadataService.getByCode(code);
        if (fileMetadata == null) {
            // 落库
            fileMetadata = new FileMetadata();
            fileMetadata.setCode(code);
            fileMetadata.setSize(uploadInfo.getSize());
            fileMetadata.setId(Long.parseLong(filename.substring(i + 1)));
            fileMetadata.setOriginalName(uploadInfo.getOriginalName());
            fileMetadata.setStorageId(uploadInfo.getStorageId());
            fileMetadata.setDirectory(directory);
            fileMetadata.setSuffix(suffix);
            AssertUtil.isTrue(fileMetadataService.save(fileMetadata), "操作失败");
        } else {
            // 落库
            fileMetadata.setOriginalName(uploadInfo.getOriginalName());
            fileMetadata.setStorageId(uploadInfo.getStorageId());
            fileMetadata.setDirectory(directory);
            fileMetadata.setSuffix(suffix);
            AssertUtil.isTrue(fileMetadataService.updateById(fileMetadata), "操作失败");
        }
        AssertUtil.isTrue(uploadInfoService.removeById(uploadId), "操作失败");
        // 完成分片上传
        template.uploadComplete(fileMetadata.filename(), uploadId, vo.getParts());
        return fileMetadata;
    }

    /**
     * 中止分片上传
     * @param uploadId
     */
    public void uploadAbort(String uploadId) {
        FileUploadInfo uploadInfo = uploadInfoService.getById(uploadId);
        AssertUtil.notNull(uploadInfo, "请先初始化");
        FileTemplate template = fileTemplateManager.getTemplate(uploadInfo.getStorageId());
        template.uploadAbort(uploadInfo.getFilename(), uploadId);
        uploadInfoService.removeById(uploadId);
    }

    /**
     * 转vo
     * @param metadata
     * @param accessMode
     * @return
     */
    public FileVO toVo(FileMetadata metadata, AccessModeEnum accessMode) {
        FileVO fileVo = new FileVO();
        fileVo.setId(metadata.getId());
        fileVo.setName(metadata.getOriginalName());
        fileVo.setSize(metadata.getSize());
        Integer storageId = metadata.getStorageId();
        if (storageId != null) {
            fileVo.setUrl(getAccessUrl(storageId, metadata.filename(), accessMode));
        }
        return fileVo;
    }

    /**
     * 获取访问路径
     * @param storageId
     * @param filename
     * @param accessMode
     * @return
     */
    public String getAccessUrl(Integer storageId, String filename, AccessModeEnum accessMode) {
        if (accessMode == null) {
            accessMode = AccessModeEnum.redirect;
        }
        switch (accessMode) {
            case sign -> {
                Date date = new Date(System.currentTimeMillis() + 2 * 3600 * 1000);
                FileTemplate template = fileTemplateManager.getTemplate(storageId);
                return template.getAccessUrl(filename, date).toString();
            }
            case direct -> {
                FileTemplate template = fileTemplateManager.getTemplate(storageId);
                return template.getUrl(filename).toString();
            }
            case proxy -> {
                return MessageFormat.format("proxy/{0}/{1}", storageId, filename);
            }
            case redirect -> {
                return MessageFormat.format("redirect/{0}/{1}", storageId, filename);
            }
            default -> {
                throw new RuntimeException("未知模式：" + accessMode);
            }
        }
    }

    public String getUploadUrl(String filename) {
        FileStorage defaulted = fileStorageService.getDefaulted();
        AssertUtil.notNull(defaulted, "请先设置默认文件仓库");
        FileTemplate template = fileTemplateManager.getTemplate(defaulted);
        String directory = getOrCreateDirectory();
        Long id = IdWorker.getId();
        String suf = filename.substring(filename.lastIndexOf("."));
        return template.getUploadUrl(directory +"/" + id + suf);
    }

    public PageResult<FileMetadataVO> query(PageQuery pageQuery, QueryParams params) {
        SqueryWapper<FileMetadata> squeryWapper  = new SqueryWapper<>(FileMetadata.class);
        PageResult<FileMetadata> query = squeryWapper.query(fileMetadataService, pageQuery, params);
        PageResult<FileMetadataVO> result = new PageResult<>();
        result.setTotal(query.getTotal());
        result.setPageNo(query.getPageNo());
        result.setPageSize(query.getPageSize());
        List<FileMetadata> source = query.getList();
        List<FileMetadataVO> target = new ArrayList<>(source.size());
        for (FileMetadata fileMetadata : source) {
            FileMetadataVO vo = new FileMetadataVO();
            PojoUtil.copy(fileMetadata, vo);
            target.add(vo);
            vo.setUrl(getAccessUrl(fileMetadata.getStorageId(), fileMetadata.filename(), AccessModeEnum.redirect));
        }
        result.setList(target);
        return result;
    }

    @Transactional(rollbackFor = Exception.class)
    public void remove(List<Long> ids) {
        List<FileMetadata> list = fileMetadataService.listByIds(ids);
        AssertUtil.isTrue(fileMetadataService.removeByIds(ids), "删除失败");
        for (FileMetadata fileMetadata : list) {
            FileTemplate template = fileTemplateManager.getTemplate(fileMetadata.getStorageId());
            template.deleteFile(fileMetadata.filename());
        }
    }

    private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyyMM");
    private String directory;
    private long expires;
    private String getOrCreateDirectory() {
        if (DataUtil.isValuable(directory) && expires > System.currentTimeMillis()) {
            return directory;
        }
        // 创建目录
        directory = LocalDate.now().format(FORMATTER);
        LocalDateTime now = LocalDateTime.now()
                .withNano(0)
                .withSecond(0)
                .withMinute(0)
                .withHour(0)
                .plusDays(1);
        expires = DatetimeUtil.timestamp(now);
        return directory;
    }

}
